﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

using NullGuard;

using static System.Text.Encoding;

#if !RootNamespace
namespace OmniKits.Extensions
{
#endif
    public static class HttpWebRequestEx
    {
#pragma warning disable 1591

        public static int DefaultBufferSize { get; set; } = 81920;
        public const int MinimumBufferSize = 4096;

        #region simple extension methods

        public static HttpWebResponse Get(this HttpWebRequest request)
        {
            request.Method = "GET";
            return (HttpWebResponse)request.GetResponse();
        }

        public static string GetResponseText(this HttpWebResponse response, Encoding encoding = null)
        {
            using (var stream = response.GetResponseStream())
            using (var reader = new StreamReader(stream, encoding ?? UTF8))
                return reader.ReadToEnd();
        }
        public static string GetResponseText(this HttpWebRequest request, Encoding encoding = null)
        {
            using (var response = (HttpWebResponse)request.GetResponse())
                return response.GetResponseText(encoding);
        }

        public static HttpWebResponse Post(this HttpWebRequest request, [AllowNull]Stream stream, int bufferSize)
        {
            if (bufferSize < MinimumBufferSize)
                bufferSize = MinimumBufferSize;

            request.Method = "POST";
            using (var requestStream = request.GetRequestStream())
            {
                if (stream != null)
                    stream.CopyTo(requestStream, bufferSize);
            }
            return (HttpWebResponse)request.GetResponse();
        }

        public static HttpWebResponse Post(this HttpWebRequest request, [AllowNull]Stream stream)
            => request.Post(stream, DefaultBufferSize);

        public static HttpWebResponse Post(this HttpWebRequest request, byte[] data = null)
        {
            request.Method = "POST";
            using (var stream = request.GetRequestStream())
            {
                if (data != null)
                    stream.Write(data, 0, data.Length);
            }
            return (HttpWebResponse)request.GetResponse();
        }

        public static HttpWebResponse Post(this HttpWebRequest request, [AllowNull]string content, Encoding encoding = null)
        {
            return request.Post((encoding ?? Encoding.UTF8).GetBytes(content ?? ""));
        }

        #endregion

        #region multipart extension methods

        #region ContentTypeParameterSpecialChars

        private static readonly char[] _ContentTypeParameterSpecialChars = new[]
        {
            ' ',
            '"',
            '(',
            ')',
            ',',
            '/',
            ';',
            ':',
            '<',
            '=',
            '>',
            '?',
            '@',
            '[',
            '\\',
            ']',
        };
        public static char[] ContentTypeParameterSpecialChars => (char[])_ContentTypeParameterSpecialChars.Clone();

        #endregion

        public static readonly Regex BoundaryPattern = new Regex(@"\s*boundary\s*=", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        private static byte[] NewLineBytes = ASCII.GetBytes("\r\n");

        public static string ExtractMultipartBoundary(this string contentType)
        {
            var tmp = contentType.Split(';').Select(s =>
            {
                var m = BoundaryPattern.Match(s);
                if (!m.Success) return null;
                return s.Substring(m.Length);
            }).SingleOrDefault(s => s != null);

            bool invalid = false;
            tmp = tmp.Trim();

            if (tmp[tmp.Length - 1] == '"')
            {
                if (tmp[0] == '"')
                {
                    tmp = tmp.Substring(1, tmp.Length - 2);
                    var sb = new StringBuilder(tmp.Length);
                    for (var i = 0; i < tmp.Length; i++)
                    {
                        var c = tmp[i];
                        if (c == '\\')
                        {
                            if (++i < tmp.Length)
                                c = tmp[i];
                            else
                                throw new InvalidDataException();
                        }
                        sb.Append(c);
                    }
                    return sb.ToString();
                }
                invalid = true;
            }
            else if (tmp[0] == '"')
            {
                invalid = true;
            }
            else if (tmp.IndexOfAny(_ContentTypeParameterSpecialChars) > -1)
            {
                invalid = true;
            }

            if (invalid)
                throw new InvalidDataException();

            return tmp;
        }

        public static string GetMultipartBoundary(this HttpWebRequest request)
        {
            return request.ContentType.ExtractMultipartBoundary();
        }

        public static void WriteMultipartFormField(this Stream stream, string boundary, string name, string value)
        {
            var s = $"--{boundary}\r\nContent-Disposition: form-data; name=\"{Uri.EscapeDataString(name)}\"\r\n\r\n{value}\r\n";
            var bytes = ASCII.GetBytes(s);
            stream.Write(bytes, 0, bytes.Length);
        }

        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string filename, Stream source, int bufferSize)
        {
            if (bufferSize < MinimumBufferSize)
                bufferSize = MinimumBufferSize;

            var s = $"--{boundary}\r\n";
            s += $"Content-Disposition: form-data; name=\"{Uri.EscapeDataString(name)}\"; ";
            s += $"filename=\"{Uri.EscapeDataString(filename)}\"\r\n";
            s += "Content-Type: application/octet-stream\r\n\r\n";

            var bytes = ASCII.GetBytes(s);
            target.Write(bytes, 0, bytes.Length);

            source.CopyTo(target, bufferSize);

            bytes = NewLineBytes;
            target.Write(bytes, 0, bytes.Length);
        }
        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string path, int bufferSize)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
                target.WriteMultipartFileField(boundary, name, Path.GetFileName(path), fs, bufferSize);
        }
        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string filename, Stream source)
            => target.WriteMultipartFileField(boundary, name, filename, source, DefaultBufferSize);
        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string path)
            => target.WriteMultipartFileField(boundary, name, path, DefaultBufferSize);

        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string filename, Stream source, long length, int bufferSize)
        {
            if (length < 0)
            {
                target.WriteMultipartFileField(boundary, name, filename, source, bufferSize);
                return;
            }

            if (bufferSize < MinimumBufferSize)
                bufferSize = MinimumBufferSize;

            var s = $"--{boundary}\r\n";
            s += $"Content-Disposition: form-data; name=\"{Uri.EscapeDataString(name)}\"; ";
            s += $"filename=\"{Uri.EscapeDataString(filename)}\"\r\n";
            s += "Content-Type: application/octet-stream\r\n\r\n";

            var bytes = ASCII.GetBytes(s);
            target.Write(bytes, 0, bytes.Length);

            bytes = new byte[bufferSize];
            int read;
            long left = length;
            do
            {
                if (left == 0) break; // end of range

                if (left < bufferSize)
                    read = (int)left;
                else
                    read = bufferSize;

                read = source.Read(bytes, 0, read);
                target.Write(bytes, 0, read);

                left -= read;
            } while (read == bufferSize); // end of file or range

            bytes = NewLineBytes;
            target.Write(bytes, 0, bytes.Length);
        }
        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string path, long length, int bufferSize)
        {
            if (length < 0)
            {
                target.WriteMultipartFileField(boundary, name, path, bufferSize);
                return;
            }

            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
                target.WriteMultipartFileField(boundary, name, Path.GetFileName(path), fs, length, bufferSize);
        }
        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string filename, Stream source, long length)
            => target.WriteMultipartFileField(boundary, name, filename, source, length, DefaultBufferSize);
        public static void WriteMultipartFileField(this Stream target, string boundary, string name, string path, long length)
            => target.WriteMultipartFileField(boundary, name, path, length, DefaultBufferSize);

        #endregion
    }

#if !RootNamespace
}
#endif
